// Written in the D Programming Language
/**
 * The 19th lesson in the <a href="http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=19">NeHe tutorial series</a>.
 * Originally written by Jeff Molofee.
 *
 * Authors: Jeff Molofee
 *          Olli Aalto
 */
module lesson19;

import derelict.opengl.gl;
import derelict.opengl.glu;
import derelict.sdl.sdl;

import tango.stdc.stringz;
import tango.math.random.Kiss;
import tango.io.Stdout;
import tango.text.convert.Sprint;

/// The window title
const char[] WINDOW_TITLE = "NeHe's Particle Tutorial (D version)";

/// Max number of particles
const MAX_PARTICLES = 1000;

Kiss kiss;

/// The main loop flag
bool running;

/// Toggle rainbow effect
bool rainbow = true;

/// Slow Down Particles 
float slowdown = 2.0f;
/// Base X Speed (To Allow Keyboard Direction Of Tail)
float xspeed = 0.0f;
/// Base Y Speed (To Allow Keyboard Direction Of Tail)
float yspeed = 0.0f;
/// Used To Zoom Out
float zoom = -40.0f;

/// Current Color Selection
GLuint color;
/// Storage For Our Particle Texture
GLuint texture;

/// Create our particle structure
struct Particle
{
	// Active (Yes/No)
	bool active;
	// Particle Life
	float life = 0.0f;
	// Fade Speed
	float fade = 0.0f;

	// Red Value
	float r = 0.0f;
	// Green Value
	float g = 0.0f;
	// Blue Value
	float b = 0.0f;

	// X Position
	float x = 0.0f;
	// Y Position
	float y = 0.0f;
	// Z Position
	float z = 0.0f;

	// X Direction
	float xi = 0.0f;
	// Y Direction
	float yi = 0.0f;
	// Z Direction
	float zi = 0.0f;

	// X Gravity
	float xg = 0.0f;
	// Y Gravity
	float yg = 0.0f;
	// Z Gravity
	float zg = 0.0f;
}

// Rainbow of colors
static GLfloat[][] colors = [[1.0f, 0.5f, 0.5f], [1.0f, 0.75f, 0.5f], [1.0f,
	1.0f, 0.5f], [0.75f, 1.0f, 0.5f], [0.5f, 1.0f, 0.5f], [0.5f, 1.0f, 0.75f],
	[0.5f, 1.0f, 1.0f], [0.5f, 0.75f, 1.0f], [0.5f, 0.5f, 1.0f], [0.75f, 0.5f,
		1.0f], [1.0f, 0.5f, 1.0f], [1.0f, 0.5f, 0.75f]];

/// Our beloved array of particles
Particle[MAX_PARTICLES] particles;

/**
 * Module constructor. Here we load the GL, GLU and SDL shared libraries,
 * and the initialize SDL.
 */
static this()
{
	DerelictGL.load();
	DerelictGLU.load();
	DerelictSDL.load();

	if (SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		throw new Exception("Failed to initialize SDL: " ~ getSDLError());
	}

	if (SDL_EnableKeyRepeat(100, SDL_DEFAULT_REPEAT_INTERVAL))
	{
		throw new Exception("Failed to set key repeat: " ~ getSDLError());
	}
}

/**
 * Module destructor. SDL_Quit must be called somewhere, and as we initialized
 * it in the module constructor so the module destructor should be a suitable
 * place.
 */
static ~this()
{
	SDL_Quit();
}

/**
 * The main function. This is where the fun begins. The first order of business
 * is the check the command line arguments if the user wanted to start in
 * fullscreen mode. Then the window is created and OpenGL is initialized with
 * basic settings. Finally the the function starts the main loop which will live
 * for the duration of the application.
 *
 * Params:
 *      args = the command line arguments
 */
void main(char[][] args)
{
	bool fullScreen = false;
	if (args.length > 1)
	{
		fullScreen = args[1] == "-fullscreen";
	}

	kiss = Kiss();

	createGLWindow(WINDOW_TITLE, 640, 480, 16, fullScreen);
	initGL();

	running = true;
	while (running)
	{
		processEvents();

		drawGLScene();

		SDL_GL_SwapBuffers();
		SDL_Delay(10);

		// If rainbow coloring is turned on, cycle the colors
		if (rainbow)
		{
			color = (++color) % 12;
		}
	}
}

/**
 * Process all the pending events.
 */
void processEvents()
{
	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
			case SDL_KEYUP:
				keyReleased(event.key.keysym.sym);
				break;
			case SDL_KEYDOWN:
				keyPressed(event.key.keysym.sym);
				break;
			case SDL_QUIT:
				running = false;
				break;
			default:
				break;
		}
	}
}

/**
 * Process key pressed events.
 * 
 * Params:
 *     key = the key that have been pressed
 */
void keyPressed(int key)
{
	switch (key)
	{
		case SDLK_KP_PLUS:
			// '+' key was pressed. This speeds up the particles.
			if (slowdown > 1.0f)
			{
				slowdown -= 0.01f;
			}
			break;
		case SDLK_KP_MINUS:
			// '-' key was pressed. This slows down the particles.
			if (slowdown < 4.0f)
			{
				slowdown += 0.01f;
			}
		case SDLK_PAGEUP:
			// PageUp key was pressed. This zooms into the scene.
			zoom += 0.01f;
			break;
		case SDLK_PAGEDOWN:
			// PageDown key was pressed. This zooms out of the scene.
			zoom -= 0.01f;
			break;
		case SDLK_UP:
			// Up arrow key was pressed. This increases the particles' y movement.
			if (yspeed < 200.0f)
			{
				yspeed++;
			}
			break;
		case SDLK_DOWN:
			// Down arrow key was pressed. This decreases the particles' y movement.
			if (yspeed > -200.0f)
			{
				yspeed--;
			}
			break;
		case SDLK_RIGHT:
			// Right arrow key was pressed. This increases the particles' x movement.
			if (xspeed < 200.0f)
			{
				xspeed++;
			}
			break;
		case SDLK_LEFT:
			// Left arrow key was pressed. This decreases the particles' x movement.
			if (xspeed > -200.0f)
			{
				xspeed--;
			}
			break;
		case SDLK_KP8:
			// NumPad 8 key was pressed. Increase particles' y gravity.
			foreach (inout particle; particles)
			{
				if (particle.yg < 1.5f)
				{
					particle.yg += 0.01f;
				}
			}
			break;
		case SDLK_KP2:
			// NumPad 2 key was pressed. Decrease particles' y gravity.
			foreach (inout particle; particles)
			{
				if (particle.yg > -1.5f)
				{
					particle.yg -= 0.01f;
				}
			}
			break;
		case SDLK_KP6:
			// NumPad 6 key was pressed. This increases the particles' x gravity.
			foreach (inout particle; particles)
			{
				if (particle.xg < 1.5f)
				{
					particle.xg += 0.01f;
				}
			}
			break;
		case SDLK_KP4:
			// NumPad 4 key was pressed. This decreases the particles' y gravity.
			foreach (inout particle; particles)
			{
				if (particle.xg > -1.5f)
				{
					particle.xg -= 0.01f;
				}
			}
			break;
		default:
			break;
	}
}

/**
 * Process a key released event.
 * 
 * Params:
 *     key = the key that have been released
 */
void keyReleased(int key)
{
	switch (key)
	{
		case SDLK_ESCAPE:
			running = false;
			break;
		case SDLK_TAB:
			// Tab key was pressed. This resets the particles and makes them re-explode.
			resetParticles();
			break;
		case SDLK_RETURN:
			// Return key was pressed. This toggles the rainbow color effect.
			rainbow = !rainbow;
			break;
		case SDLK_SPACE:
			// Spacebar was pressed. This turns off rainbow-ing and manually cycles through colors.
			rainbow = false;
			color = (++color) % 12;
			break;
		default:
			break;
	}
}

/**
 * Function to load in bitmap as a GL texture.
 */
void loadGLTextures()
{
	SDL_Surface* textureImage;

	// Load The Bitmap, Check For Errors, If Bitmap's Not Found Quit
	if ((textureImage = SDL_LoadBMP("data/particle.bmp")) !is null)
	{
		// Free the surface when we exit the scope
		scope(exit)
			SDL_FreeSurface(textureImage);

		// Create The Textures
		glGenTextures(1, &texture);

		// Create Linear Filtered Texture
		glBindTexture(GL_TEXTURE_2D, texture);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexImage2D(GL_TEXTURE_2D, 0, 3, textureImage.w, textureImage.h, 0,
			GL_BGR, GL_UNSIGNED_BYTE, textureImage.pixels);
		return;
	}
	throw new Exception("Failed to load texture.");
}

/**
 * Reset all the particles.
 */
void resetParticles()
{
	for (int loop = 0; loop < MAX_PARTICLES; loop++)
	{
		int col = (loop + 1) / (MAX_PARTICLES / 11);
		float xi = ((kiss.toInt() % 50) - 26.0f) * 10.0f;
		float yi = ((kiss.toInt() % 50) - 25.0f) * 10.0f;
		resetParticle(loop, col, xi, yi, yi);
	}
}

/** 
 * Function to reset one particle to initial state.
 * 
 * NOTE: I added this function to replace doing the same thing in several
 * places and to also make it easy to move the pressing of numpad keys
 * 2, 4, 6, and 8 into handleKeyPress function.
 */
void resetParticle(int num, int col, float xDir, float yDir, float zDir)
{
	// Make the particels active
	particles[num].active = true;
	// Give the particles life 
	particles[num].life = 1.0f;
	// Random Fade Speed
	particles[num].fade = (kiss.toInt() % 100) / 1000.0f + 0.003f;
	// Select Red Rainbow Color 
	particles[num].r = colors[col][0];
	// Select Green Rainbow Color
	particles[num].g = colors[col][1];
	// Select Blue Rainbow Color 
	particles[num].b = colors[col][2];
	// Set the position on the X axis 
	particles[num].x = 0.0f;
	// Set the position on the Y axis 
	particles[num].y = 0.0f;
	// Set the position on the Z axis 
	particles[num].z = 0.0f;
	// Random Speed On X Axis
	particles[num].xi = xDir;
	// Random Speed On Y Axi 
	particles[num].yi = yDir;
	// Random Speed On Z Axis
	particles[num].zi = zDir;
	// Set Horizontal Pull To Zero
	particles[num].xg = 0.0f;
	// Set Vertical Pull Downward 
	particles[num].yg = -0.8f;
	// Set Pull On Z Axis To Zero 
	particles[num].zg = 0.0f;
}

/**
 * Resize and initialize the OpenGL window.
 */
void resizeGLScene(GLsizei width, GLsizei height)
{
	if (height == 0)
	{
		height = 1;
	}
	// Reset The Current Viewport
	glViewport(0, 0, width, height);

	// Select The Projection Matrix
	glMatrixMode(GL_PROJECTION);

	// Reset The Projection Matrix
	glLoadIdentity();

	// Calculate The Aspect Ratio Of The Window
	gluPerspective(45.0f, cast(GLfloat) width / cast(GLfloat) height, 0.1f,
		100.0f);

	// Select The Modelview Matrix
	glMatrixMode(GL_MODELVIEW);

	// Reset The Modelview Matrix
	glLoadIdentity();
}

/**
 * Initialize OpenGL.
 */
void initGL()
{
	loadGLTextures();

	// Enables Smooth Shading
	glShadeModel(GL_SMOOTH);
	// Black Background
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	// Depth Buffer Setup
	glClearDepth(1.0f);
	// Disables Depth Testing
	glDisable(GL_DEPTH_TEST);
	// Really Nice Perspective Calculations
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
	// Really Nice Point Smoothing
	glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
	// Enable Blending
	glEnable(GL_BLEND);
	// Type Of Blending To Perform
	glBlendFunc(GL_SRC_ALPHA, GL_ONE);

	/* Enable Texture Mapping */
	glEnable(GL_TEXTURE_2D);
	/* Select Our Texture */
	glBindTexture(GL_TEXTURE_2D, texture);

	// Change to texture matrix and flip and rotate the texture
	glMatrixMode(GL_TEXTURE);
	glRotatef(180.0f, 0.0f, 0.0f, 1.0f);
	glScalef(-1.0f, 1.0f, 1.0f);
	// Back to normal
	glMatrixMode(GL_MODELVIEW);

	// Reset all the particles
	resetParticles();
}

/**
 * The drawing function. Now we only clear the color and depht buffers, so that
 * the window stays black.
 */
void drawGLScene()
{
	// Clear The Screen And The Depth Buffer
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glLoadIdentity();

	// Modify each of the particles
	foreach (loop, inout particle; particles)
	{
		if (particle.active)
		{
			// Grab Our Particle X Position
			float x = particle.x;
			// Grab Our Particle Y Position
			float y = particle.y;
			// Particle Z Position + Zoom 
			float z = particle.z + zoom;

			/* Draw The Particle Using Our RGB Values,
			 * Fade The Particle Based On It's Life
			 */
			glColor4f(particle.r, particle.g, particle.b, particle.life);

			// Build Quad From A Triangle Strip
			glBegin(GL_TRIANGLE_STRIP);
			// Top Right 
			glTexCoord2d(1, 1);
			glVertex3f(x + 0.5f, y + 0.5f, z);
			// Top Left 
			glTexCoord2d(0, 1);
			glVertex3f(x - 0.5f, y + 0.5f, z);
			// Bottom Right 
			glTexCoord2d(1, 0);
			glVertex3f(x + 0.5f, y - 0.5f, z);
			// Bottom Left 
			glTexCoord2d(0, 0);
			glVertex3f(x - 0.5f, y - 0.5f, z);
			glEnd();

			// Move On The X Axis By X Speed 
			particle.x += particle.xi / (slowdown * 1000);
			// Move On The Y Axis By Y Speed 
			particle.y += particle.yi / (slowdown * 1000);
			// Move On The Z Axis By Z Speed 
			particle.z += particle.zi / (slowdown * 1000);

			// Take Pull On X Axis Into Account 
			particle.xi += particle.xg;
			// Take Pull On Y Axis Into Account 
			particle.yi += particle.yg;
			// Take Pull On Z Axis Into Account 
			particle.zi += particle.zg;

			// Reduce Particles Life By 'Fade' 
			particle.life -= particle.fade;

			// If the particle dies, revive it
			if (particle.life < 0.0f)
			{
				float xi = xspeed + (kiss.toInt() % 60) - 32.0f;
				float yi = yspeed + (kiss.toInt() % 60) - 30.0f;
				float zi = (kiss.toInt() % 60) - 30.0f;
				resetParticle(loop, color, xi, yi, zi);
			}
		}
	}

	calculateFps();
}

/**
 * Calculates the FPS and outputs it to the cosole.
 */
void calculateFps()
{
	// These are to calculate our fps
	static GLint T0;
	static GLint frames;

	/* Gather our frames per second */
	frames++;
	GLint time = SDL_GetTicks();
	if ((time - T0) >= 5000)
	{
		GLfloat seconds = (time - T0) / 1000.0;
		GLfloat fps = frames / seconds;

		// Create a Sprint instance
		auto sprint = new Sprint!(char);
		// Write formatted text to the console
		Stdout(sprint("{0} frames in {1} seconds = {2} FPS", frames, seconds,
			fps)).newline;
		T0 = time;
		frames = 0;
	}
}

/**
 * Initializes and opens the SDL window.
 */
void createGLWindow(char[] title, int width, int height, int bits,
	bool fullScreen)
{
	// Set the OpenGL attributes
	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	// Set the window title
	SDL_WM_SetCaption(toStringz(title), null);

	// Note the SDL_DOUBLEBUF flag is not required to enable double
	// buffering when setting an OpenGL video mode.
	// Double buffering is enabled or disabled using the
	// SDL_GL_DOUBLEBUFFER attribute. (See above.)
	int mode = SDL_OPENGL;
	if (fullScreen)
	{
		mode |= SDL_FULLSCREEN;
	}
	// Now open a SDL OpenGL window with the given parameters
	if (SDL_SetVideoMode(width, height, bits, mode) is null)
	{
		throw new Exception("Failed to open OpenGL window: " ~ getSDLError());
	}

	resizeGLScene(width, height);
}

/**
 * Get the SDL error as a D string.
 *
 * Returns: A D string containing the current SDL error.
 */
char[] getSDLError()
{
	return fromStringz(SDL_GetError());
}
